package visitor;

import java.util.ArrayList;
import java.util.List;

import semantic.tables.ClassTable;
import semantic.tables.ErrorMsg;
import semantic.tables.MethodTable;
import semantic.tables.ProgramTable;
import syntaxtree.AbstractNode;
import syntaxtree.And;
import syntaxtree.ArrayAssign;
import syntaxtree.ArrayLength;
import syntaxtree.ArrayLookup;
import syntaxtree.Assign;
import syntaxtree.Block;
import syntaxtree.BooleanType;
import syntaxtree.Call;
import syntaxtree.ClassDeclExtends;
import syntaxtree.ClassDeclSimple;
import syntaxtree.Exp;
import syntaxtree.False;
import syntaxtree.Formal;
import syntaxtree.Identifier;
import syntaxtree.IdentifierExp;
import syntaxtree.IdentifierType;
import syntaxtree.If;
import syntaxtree.IntArrayType;
import syntaxtree.IntegerLiteral;
import syntaxtree.IntegerType;
import syntaxtree.LessThan;
import syntaxtree.MainClass;
import syntaxtree.MethodDecl;
import syntaxtree.Minus;
import syntaxtree.NewArray;
import syntaxtree.NewObject;
import syntaxtree.Not;
import syntaxtree.Plus;
import syntaxtree.Print;
import syntaxtree.Program;
import syntaxtree.This;
import syntaxtree.Times;
import syntaxtree.True;
import syntaxtree.Type;
import syntaxtree.VarDecl;
import syntaxtree.While;


public class SymbolTableTypeCheckVisitor implements TypeVisitor {

	private final ProgramTable programTable;
	private ClassTable currClass;
	private MethodTable currMethod;
	public ErrorMsg error;

	public SymbolTableTypeCheckVisitor(ProgramTable programTable) {
		this.programTable = programTable;
	}

	@Override
	public Type visit(AbstractNode n) {
		return (Type) VisitorUtil.visit(this, n);
	}

	// MainClass m;
	// ClassDeclList cl;
	public Type visit(Program n) {
		error = new ErrorMsg();

		try {
			n.mainClass.accept(this);
			for (int i = 0; i < n.classList.size(); i++) {
				n.classList.elementAt(i).accept(this);
			}
		} catch (Exception e) {
			// Check if an unexpected error has occurred:
			if (!error.anyErrors) {
				e.printStackTrace();
			}
		}
		return null;
	}

	// Identifier i1,i2;
	// Statement s;
	public Type visit(MainClass n) {
		// n.idClass.accept(this);

		// Sets the current class:
		currClass = programTable.getClass(n.idClass.str);

		n.idMainArgs.accept(this);
		n.stm.accept(this);

		// After the class scope, sets the current class table to null:
		currClass = null;
		return null;
	}

	// Identifier i;
	// VarDeclList vl;
	// MethodDeclList ml;
	public Type visit(ClassDeclSimple n) {
		// n.id.accept(this);

		// Sets the current class:
		currClass = programTable.getClass(n.id.str);

		for (int i = 0; i < n.varList.size(); i++) {
			n.varList.elementAt(i).accept(this);
		}
		for (int i = 0; i < n.methodList.size(); i++) {
			n.methodList.elementAt(i).accept(this);
		}

		// After the class scope, sets the current class table to null:
		currClass = null;
		return null;
	}

	// Identifier i;
	// Identifier j;
	// VarDeclList vl;
	// MethodDeclList ml;
	public Type visit(ClassDeclExtends n) {
		// n.id.accept(this);
		// n.idSuperClass.accept(this);

		// Type check the super class:
		if (programTable.getClass(n.idSuperClass.str) == null) {
			addErrorCannotBeResolvedToAType(n.idSuperClass.str);
		}

		// Sets the current class:
		currClass = programTable.getClass(n.id.str);

		for (int i = 0; i < n.varList.size(); i++) {
			n.varList.elementAt(i).accept(this);
		}
		for (int i = 0; i < n.methodList.size(); i++) {
			n.methodList.elementAt(i).accept(this);
		}

		// After the class scope, sets the current class table to null:
		currClass = null;
		return null;
	}

	// Type t;
	// Identifier i;
	public Type visit(VarDecl n) {
		// n.type.accept(this);
		// n.id.accept(this);
		return null;
	}

	// Type t;
	// Identifier i;
	// FormalList fl;
	// VarDeclList vl;
	// StatementList sl;
	// Exp e;
	public Type visit(MethodDecl n) {
		n.returnType.accept(this);
		n.id.accept(this);

		// Sets the current class:
		currMethod = currClass.getMethod(n.id.str);

		for (int i = 0; i < n.formalList.size(); i++) {
			n.formalList.elementAt(i).accept(this);
		}
		for (int i = 0; i < n.varList.size(); i++) {
			n.varList.elementAt(i).accept(this);
		}
		for (int i = 0; i < n.stmList.size(); i++) {
			n.stmList.elementAt(i).accept(this);
		}

		// Type check the return expression:
		Type returnExpType = n.returnExp.accept(this);
		if (!n.returnType.getClass().equals(returnExpType.getClass())) {
			addErrorTypeMismatch(n.returnType, returnExpType);
		}

		// After the method scope, sets the current method table to null:
		currMethod = null;
		return null;
	}

	// Type t;
	// Identifier i;
	public Type visit(Formal n) {
		n.type.accept(this);
		n.id.accept(this);
		return null;
	}

	public Type visit(IntArrayType n) {
		return n;
	}

	public Type visit(BooleanType n) {
		return n;
	}

	public Type visit(IntegerType n) {
		return n;
	}

	// String s;
	public Type visit(IdentifierType n) {
		return n;
	}

	// StatementList sl;
	public Type visit(Block n) {
		for (int i = 0; i < n.stmList.size(); i++) {
			n.stmList.elementAt(i).accept(this);
		}
		return null;
	}

	// Exp e;
	// Statement s1,s2;
	public Type visit(If n) {

		Type expType = n.e.accept(this);
		if (!(expType instanceof BooleanType)) {
			addErrorTypeMismatch(new BooleanType(), expType);
		}
		n.s1.accept(this);
		n.s2.accept(this);

		return null;
	}

	// Exp e;
	// Statement s;
	public Type visit(While n) {
		Type expType = n.e.accept(this);
		if (!(expType instanceof BooleanType)) {
			addErrorTypeMismatch(new BooleanType(), expType);
		}
		n.s.accept(this);
		return null;
	}

	// Exp e;
	public Type visit(Print n) {
		n.e.accept(this);
		return null;
	}

	// Identifier i;
	// Exp e;
	public Type visit(Assign n) {
		Type idType = n.i.accept(this);
		Type expType = n.e.accept(this);
		if (!idType.getClass().equals(expType.getClass())) {
			addErrorTypeMismatch(idType, expType);
		} else if (idType instanceof IdentifierType) {
			IdentifierType idIdentifierType = (IdentifierType) idType;
			IdentifierType expIdentifierType = (IdentifierType) idType;
			if (!idIdentifierType.id.equals(expIdentifierType.id)) {
				addErrorTypeMismatch(idType, expType);
			}
		}
		return null;
	}

	// Identifier i;
	// Exp e1,e2;
	public Type visit(ArrayAssign n) {
		Type arrayVarType = n.i.accept(this);
		Type indexType = n.e1.accept(this);
		Type valueType = n.e2.accept(this);
		if (!(arrayVarType instanceof IntArrayType)) {
			addErrorTypeMismatch(new IntArrayType(), arrayVarType);
		}
		if (!(indexType instanceof IntegerType)) {
			addErrorTypeMismatch(new IntegerType(), indexType);
		}
		if (!(valueType instanceof IntegerType)) {
			addErrorTypeMismatch(new IntegerType(), valueType);
		}
		return null;
	}

	// Exp e1,e2;
	public Type visit(And n) {
		Type e1Type = n.e1.accept(this);
		Type e2Type = n.e1.accept(this);
		if (!(e1Type instanceof BooleanType)) {
			addErrorTypeMismatch(new BooleanType(), e1Type);
		}
		if (!(e2Type instanceof BooleanType)) {
			addErrorTypeMismatch(new BooleanType(), e2Type);
		}
		return new BooleanType();
	}

	// Exp e1,e2;
	public Type visit(LessThan n) {

		Type e1 = n.e1.accept(this);
		Type e2 = n.e2.accept(this);
		if (!(e1 instanceof IntegerType)) {
			addErrorOperation(n, true);
		}
		if (!(e2 instanceof IntegerType)) {
			addErrorOperation(n, false);
		}
		return new BooleanType();
	}

	// Exp e1,e2;
	public Type visit(Plus n) {
		Type e1 = n.e1.accept(this);
		Type e2 = n.e2.accept(this);
		if (!(e1 instanceof IntegerType)) {
			addErrorOperation(n, true);
		}
		if (!(e2 instanceof IntegerType)) {
			addErrorOperation(n, false);
		}
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(Minus n) {
		Type e1 = n.e1.accept(this);
		Type e2 = n.e2.accept(this);
		if (!(e1 instanceof IntegerType)) {
			addErrorOperation(n, true);
		}
		if (!(e2 instanceof IntegerType)) {
			addErrorOperation(n, false);
		}
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(Times n) {
		Type e1 = n.e1.accept(this);
		Type e2 = n.e2.accept(this);
		if (!(e1 instanceof IntegerType)) {
			addErrorOperation(n, true);
		}
		if (!(e2 instanceof IntegerType)) {
			addErrorOperation(n, false);
		}
		return new IntegerType();
	}

	// Exp e1,e2;
	public Type visit(ArrayLookup n) {
		Type arrayExpType = n.e1.accept(this);
		Type indexExpType = n.e2.accept(this);
		if (!(arrayExpType instanceof IntArrayType)) {
			addErrorTypeMismatch(new IntArrayType(), arrayExpType);
		}
		if (!(indexExpType instanceof IntegerType)) {
			addErrorTypeMismatch(new IntegerType(), indexExpType);
		}
		return new IntegerType();
	}

	// Exp e;
	public Type visit(ArrayLength n) {
		Type arrayExpType = n.e1.accept(this);
		if (!(arrayExpType instanceof IntArrayType)) {
			addErrorTypeMismatch(new IntArrayType(), arrayExpType);
		}
		return new IntegerType();
	}

	// Exp e;
	// Identifier i;
	// ExpList el;
	public Type visit(Call n) {
		Type objType = n.e1.accept(this);
		if (!(objType instanceof IdentifierType)) {
			addErrorTypeMismatch(new IdentifierType("object"), objType);
		}
		IdentifierType objClassType = (IdentifierType) objType;

		// Lista com tipos dos parâmetros:
		List<Type> formalTypeList = new ArrayList<Type>();
		for (int i = 0; i < n.el.size(); i++) {
			Type formalType = n.el.elementAt(i).accept(this);
			formalTypeList.add(formalType);
		}

		String methodName = n.i1.str;
		// n.i1.accept(this); //If called, it will search the method in current scope class

		String className = objClassType.id;
		ClassTable classTable = programTable.getClass(className);
		MethodTable methodTable = classTable.getMethod(methodName);

		String lastIncompatibleFormalsError;
		do {
			// Verifies if the method was declared and there is no more super classes to check:
			if (methodTable == null && classTable.superClass == null) {
				addErrorMethodUndefined(methodName, objClassType.id);
				return null;
			}
			if (methodTable.formalTypeListEquals(formalTypeList)) {
				return methodTable.returnType;
			} else {
				lastIncompatibleFormalsError = "The method " + methodName + " in the type " + className
						+ " is not applicable for the arguments " + formalTypeList.toString();
			}
		} while (classTable.superClass != null);
		error.error(lastIncompatibleFormalsError);
		return null;
	}

	// int i;
	public Type visit(IntegerLiteral n) {
		return new IntegerType();
	}

	public Type visit(True n) {
		return new BooleanType();
	}

	public Type visit(False n) {
		return new BooleanType();
	}

	// String s;
	public Type visit(IdentifierExp n) {
		Type type = null;
		if (currMethod == null) {
			type = currClass.getVarType(n.s1);
		} else {
			type = currMethod.getVarType(n.s1);
			if (type == null) {
				type = currClass.getVarType(n.s1);
			}
		}
		if (type == null) {
			if (programTable.getClass(n.s1) != null) {
				type = new IdentifierType(n.s1);
			}
		}
		return type;
	}

	public Type visit(This n) {
		IdentifierType type = new IdentifierType(currClass.id.toString());
		return type;
	}

	// Exp e;
	public Type visit(NewArray n) {
		Type type = n.e1.accept(this);
		if (!(type instanceof IntegerType)) {
			addErrorTypeMismatch(new IntegerType(), type);
		}
		return new IntArrayType();
	}

	// Identifier i;
	public Type visit(NewObject n) {
		return new IdentifierType(n.i1.str);
	}

	// Exp e;
	public Type visit(Not n) {
		Type expType = n.e1.accept(this);
		if (!(expType instanceof BooleanType)) {
			addErrorTypeMismatch(new BooleanType(), expType);
		}
		return new BooleanType();
	}

	// String s;
	public Type visit(Identifier n) {
		Type type = null;
		if (currMethod == null) {
			type = currClass.getVarType(n.str);
		} else {
			type = currMethod.getVarType(n.str);
			if (type == null) {
				type = currClass.getVarType(n.str);
			}
		}
		return type;
	}

	private void addErrorOperation(Exp operation, boolean isLeftSide) {
		String operationString = "";
		if (operation instanceof LessThan) {
			operationString = "LessThan";
		}
		if (operation instanceof Plus) {
			operationString = "Plus";
		}
		if (operation instanceof Minus) {
			operationString = "Minus";
		}
		if (operation instanceof Times) {
			operationString = "Times";
		}
		if (isLeftSide) {
			error.error("Left side of " + operationString + " must be of type integer");
		} else {
			error.error("Right side of " + operationString + " must be of type integer");
		}

	}

	private void addErrorCannotBeResolvedToAType(String id) {
		error.error(id + " cannot be resolved to a type");
	}

	private void addErrorTypeMismatch(Type expected, Type found) {
		error.error("Type mismatch: cannot convert from " + found.toString() + " to " + expected.toString());
	}

	private void addErrorMethodUndefined(String methodName, String className) {
		error.error("The method " + methodName + " is undefined for the type " + className);
	}
}
